/*
This file contains the implementation of a number of procedures to generate
random type graph either for families of instances or stand-alone graphs.

Copyright (C) 2018  Denis Pankratov

Modifications Copyright (C) 2019 Christodoulos Karavasilis

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "stdafx.h"
#include "bmcommon.h"
#include "graph.h"
#include <cstdlib>
#include <set>
#include <random>
#include "randomgraphgen.h"
using namespace std;


/*
Generates a sample of the binomial random variable with parameters n and p in
expected time O(np).
Uses second waiting moment from page 552 Devroye's book
*/
int getBinomial(int n, double p) {
	double log_q = log(1.0 - p);
	int x = 0;
	double sum = 0.0;
	while (true) {
		sum += log(rand_frac(rng)) / (n - x);
		if (sum < log_q)
			return x;
		x++;
	}
	return 0;
}

/*
Generates a sample of the binomial random variable with parameters n and p 
in time O(n). Uses the trivial algorithm.
*/
int getBinomialSlow(int n, double p) {
	int res = 0;
	for (int i = 0; i < n; i++)
		if (rand_frac(rng) < p)
			res++;
	return res;
}

/*
Generate k uniform random numbers between 0 and n
*/
set<int> generateRandomNeighbors(int n, int k) {
	set<int> res;
	uniform_int_distribution<int> uni(0, n - 1);
	while ((int)res.size() < k) {
		res.insert(uni(rng));
	}
	return res;
}

/*
Returns a bipartite Erdos-Renyi graph where

n - number of online nodes
m - number of online nodes
p - probability that an edge (i,j) is generated
*/
Graph bipartiteErdosRenyi(int n, int m, double p) {
	Graph g(n + m);
	g.setLeftSize(n);

	// fast version
	for (int i = 0; i < n; i++) {
		set<int> nbs = generateRandomNeighbors(m, getBinomial(m, p));
		for (set<int>::iterator it = nbs.begin(); it != nbs.end(); it++) {
			g.addEdge(i, n + *it);
		}
	}

	return g;
}

/*
Returns a random graph that is d-regular on the left or d-regular on the right.
The graph has n nodes on the left, m on the right.
We assume that d is much less than n.
*/
Graph generateDRegular(int n, int m, int d, bool leftRegular) {
	Graph g(n + m);
	g.setLeftSize(n);

	uniform_int_distribution<int> distL(0, n - 1);
	uniform_int_distribution<int> distR(0, m - 1);
	int range = (leftRegular) ? n : m;
	for (int i = 0; i < range; i++) {
		int from = (leftRegular) ? i : (n + i);
		set<int> nbs;
		while (nbs.size() < d) {
			int nb = (leftRegular) ? (n + distR(rng)) : distL(rng);
			nbs.insert(nb);
		}
		for (auto it = nbs.begin(); it != nbs.end(); it++)
			g.addEdge(from, *it);
	}
	return g;
}


/*
Generates a triangular graph that is the worst case graph for
RANKING (see KVV paper), where n is the number of online nodes.
*/
Graph triangular(int n) {
	Graph g(2 * n);
	g.setLeftSize(n);
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n - i; j++) {
			g.addEdge(i, n + j);
		}
	}
	return g;
}

/*
Generate hexa type graph.
*/
Graph hexa(int b) {
	uniform_int_distribution<int> uni(0, b - 1);
	int n = b * b;
	Graph res(n + n);
	res.setLeftSize(n);
	for (int bl = 0; bl < b; bl++) {
		for (int br = 0; br < b; br++) {
			set<int> left, right;
			while (left.size() < 3)
				left.insert(uni(rng));
			while (right.size() < 3)
				right.insert(uni(rng));
			vector<int> leftRand(left.begin(), left.end());
			vector<int> rightRand(right.begin(), right.end());
			shuffle(leftRand.begin(), leftRand.end(), rng);
			shuffle(rightRand.begin(), rightRand.end(), rng);

			res.addEdge(bl*b + leftRand[0], n + br * b + rightRand[0]);
			res.addEdge(bl*b + leftRand[1], n + br * b + rightRand[1]);
			res.addEdge(bl*b + leftRand[2], n + br * b + rightRand[2]);

			res.addEdge(bl*b + leftRand[0], n + br * b + rightRand[1]);
			res.addEdge(bl*b + leftRand[1], n + br * b + rightRand[2]);
			res.addEdge(bl*b + leftRand[2], n + br * b + rightRand[0]);
		}
	}
	//res.printDebug();
	return res;
}

/*
Create a worst case Feldman et al.
n should be a multiple of 4!
*/
Graph worstCaseFeldman(int n) {
	assert(n % 4 == 0);

	Graph res(2 * n);
	res.setLeftSize(n);

	/*add cycles*/
	for (int i = 0; i < n / 4; i++) {
		res.addEdge(3 * i, n + 3 * i);
		res.addEdge(3 * i + 1, n + 3 * i + 1);
		res.addEdge(3 * i + 2, n + 3 * i + 2);

		res.addEdge(3 * i, n + 3 * i + 1);
		res.addEdge(3 * i + 1, n + 3 * i + 2);
		res.addEdge(3 * i + 2, n + 3 * i);
	}

	/*add complete bigraphs*/
	for (int i = 0; i < n / 4; i++) {
		for (int j = 0; j < n / 4; j++) {
			res.addEdge(3 * n / 4 + i, n + 3 * j + 2);
			res.addEdge(3 * i, n + 3 * n / 4 + j);
		}
	}
	return res;
}

/*
Generate rope graph from Augment or Push paper
with k blocks of size d each
*/
Graph rope(int k, int d) {
	int n = k * d;
	Graph res(n + n);
	res.setLeftSize(n);
	double prob = 1.0 - 1.0 / d;
	uniform_real_distribution<double> curDist(0, 1);

	for (int bl = 0; bl < k - 1; bl++) {
		if (bl % 2 == 0) {
			vector<int> temp;
			for (int i = 0; i < d; i++) {
				temp.push_back((bl + 1)*d + i);
			}
			random_shuffle(temp.begin(), temp.end());
			for (int i = 0; i < d; i++) {
				res.addEdge(bl*d + i, n + temp[i]);
			}
		}
		else {
			for (int i = 0; i < d; i++) {
				for (int j = 0; j < d; j++) {
					if (curDist(rng) < prob) {
						res.addEdge(bl*d + i, n + (bl + 1)*d + j);
					}
				}
			}
		}
	}

	if (k % 2 == 1) {
		vector<int> temp;
		for (int i = 0; i < d; i++) {
			temp.push_back((k - 1)*d + i);
		}
		random_shuffle(temp.begin(), temp.end());
		for (int i = 0; i < d; i++) {
			res.addEdge((k - 1)*d + i, n + temp[i]);
		}
	}
	else {
		for (int i = 0; i < d; i++) {
			for (int j = 0; j < d; j++) {
				if (curDist(rng) < prob) {
					res.addEdge((k - 1)*d + i, n + (k - 1)*d + j);
				}
			}
		}
	}

	return res;
}

/*Creates a zipf graph from Augment or Push paper*/
Graph zipf(int n, int d) {
	Graph res(n + n);
	res.setLeftSize(n);

	uniform_real_distribution<double> curDist(0, 1);
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			double p = min(n*d / (log(n)*log(n)*(i + 1)*(j + 1)), 1.0);
			if (curDist(rng) < p) {
				res.addEdge(i, n + j);
			}
		}
	}

	return res;
}

/*
Fewg and manyg graphs from Augment or Push paper with
g groups of size sz each.
*/
Graph fewgAndManyg(int g, int sz, int d) {
	int n = g * sz;
	Graph res(n + n);
	res.setLeftSize(n);

	vector<int> lefty(n, 0);
	for (int i = 0; i < n; i++)
		lefty[i] = i;
	random_shuffle(lefty.begin(), lefty.end());

	uniform_real_distribution<double> curDist(0, 1);
	double p = (double)d / (3.0 * sz);
	for (int gl = 0; gl < g; gl++) {
		for (int i = 0; i < sz; i++) {
			vector<int> grs;
			if (gl == 0) {
				grs.push_back(g - 1);
			}
			else {
				grs.push_back(gl - 1);
			}
			grs.push_back(gl);
			if (gl == (g - 1)) {
				grs.push_back(0);
			}
			else {
				grs.push_back(gl + 1);
			}
			for (int z = 0; z < (int)grs.size(); z++) {
				int gr = grs[z];
				for (int j = 0; j < sz; j++) {
					if (curDist(rng) < p) {
						res.addEdge(gl*sz + i, n + gr * sz + j);
					}
				}
			}
		}
	}

	return res;
}


/*
Returns ManshadiEtAl graph that is current leader of worst-case graphs
for all integral known i.i.d. graphs.
*/
Graph manshadiWorstCase(int n) {
	int k = n / exp(1.0);
	Graph res(n + k + n);
	res.setLeftSize(n + k);
	for (int i = 0; i < n; i++) {
		res.addEdge(i, n + k + i);
	}
	for (int i = n; i < n + k; i++) {
		for (int j = n + k; j < n + k + n; j++) {
			res.addEdge(i, j);
		}
	}

	return res;
}


/*
Returns a complete bipartite graph with n online nodes and m offline nodes.
*/
Graph bipartiteComplete(int n, int m) {
	Graph g(n + m);
	g.setLeftSize(n);
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++) {
			g.addEdge(i, n + j);
		}
	}
	return g;
}


/*
Samples a number between 0 and cdf.size()-1 (inclusive) where number i appears 
with probability cdf[i]-cdf[i-1], where cdf[-1] = 0.0. The cdf vector must 
satisfy that cdf[i] >= cdf[i-1] and cdf[cdf.size()-1] = 1.0.
cdf must have size  > 0.
*/
int sampleFromCDF(const vector<double> &cdf) {
	assert(cdf.size() > 0);
	uniform_real_distribution<double> uni(0.0, 1.0);
	auto it = lower_bound(cdf.begin(), cdf.end(), uni(rng));
	return it - cdf.begin();
}


/*
Create preferential attachment bigraph
*/
Graph preferentialAttachmentBiGraph(int n, double c) {
	Graph res(n + n);
	res.setLeftSize(n);
	vector<int> degs(n, 1);
	int totDeg = n;

	for (int i = 0; i < n; i++) {
		int numNbs = getBinomialSlow(n, c / n);


		vector<double> cdf(n, 0.0);
		for (int j = 0; j < n; j++) {
			if (j >= 1) cdf[j] = cdf[j - 1];
			cdf[j] += 1.0*degs[j] / totDeg;
			//cerr << " j: " << j << " CDF: " << cdf[j] << endl;
		}

		cdf[n - 1] = 1.0;

		set<int> nbs;
		while (nbs.size() < numNbs)
			nbs.insert(sampleFromCDF(cdf));

		for (auto it = nbs.begin(); it != nbs.end(); it++) {
			//cerr << "NBS: " << *it << endl;
			res.addEdge(i, n + *it);
			degs[*it]++;
		}
		totDeg += numNbs;
	}


	return res;
}


/*
Returns a vector of length m, where each entry is a random number
between 0 and n.
*/
vector<int> iid(int n, int m) {
	vector<int> result;
	uniform_int_distribution<int> uni(0, n - 1);
	for (int i = 0; i < m; i++) {
		result.push_back(uni(rng));
	}
	return result;
}



/*
Generates the CDF for the power law distribution with the given exponent and
cutoff. Goes until the increments in cdf become super small. Thus, has finite 
support.
*/
vector<double> powerLawCDF(double exponent, double cutoff) {
	vector<double> res;
	double accuracy = 0.0000000001;
	res.push_back(0.0);
	int k = 1;
	do {
		double curVal = pow(k, -exponent)*exp(-k / cutoff);
		//cerr << curVal << endl;
		if (curVal < accuracy)
			break;
		res.push_back(res[k - 1] + curVal);
		k++;
	} while (true);
	double tot = res[(int)res.size() - 1];

	for (int k = 0; k < (int)res.size(); k++) {
		res[k] /= tot;
	}
	return res;
}



/*
Given a CDF for the degree distribution, constrcut a graph on n nodes using 
Molloy-Reed construction. If randomBipartition is set to true, the graph
returned is obtained by first doing the above construction, then partitioning
nodes randomly into two balanced sets and keeping the edges only between sets.
*/
Graph generateMolloyReed(int n, const vector<double> & degCDF, 
	bool randomBipartition) {
	int rep = 100; // number of repetitions per node to try and make
	               // the graph simple
	Graph g(n);

	if (randomBipartition)
		g.setLeftSize(n / 2);
	else
		g.setLeftSize(n);

	vector<int> degs(n, 0);

	int totalDeg = 0;
	for (int i = 0; i < n; i++) {
		int curdeg = sampleFromCDF(degCDF);
		while (curdeg >= n - 1) { // make sure to get something reasonable
			curdeg = sampleFromCDF(degCDF);
		}
		degs[i] = curdeg;
		totalDeg += curdeg;
	}

	uniform_int_distribution<int> randNode(0, n - 1);

	// Resample a random node's degree if total degree is odd
	while (totalDeg % 2) {
		int nodeToResample = randNode(rng);

		int curdeg = sampleFromCDF(degCDF);
		while (curdeg >= n - 1) {
			curdeg = sampleFromCDF(degCDF);
		}

		totalDeg -= degs[nodeToResample];
		degs[nodeToResample] = curdeg;
		totalDeg += curdeg;
	}

	// throw in edges!
	set<pair<int, int> > edges;
	vector<int> nodes;
	for (int i = 0; i < n; i++)
		if (degs[i] > 0)
			nodes.push_back(i);

	while (totalDeg > 0) {
		// connect two random endpoints
		int from, to;
		int fromi, toi;
		pair<int, int> edge;
		uniform_int_distribution<int> randNode2(0, (int)nodes.size() - 1);

		// try to create a simple graph
		for (int zz = 0; zz < rep; zz++) {
			vector<int> two;
			fromi = randNode2(rng);
			toi = randNode2(rng);
			from = nodes[fromi];
			to = nodes[toi];

			if (from > to) {
				int temp = from;
				from = to;
				to = temp;
				temp = fromi;
				fromi = toi;
				toi = temp;
			}

			edge = make_pair(from, to);

			if (from == to) continue;
			if (edges.find(edge) != edges.end()) continue;

			break;
		}

		if (from != to && edges.find(edge) == edges.end()) {
			edges.insert(edge);
			if (randomBipartition) {
				if (edge.first < n / 2 && edge.second >= n / 2) {
					g.addEdge(edge.first, edge.second);
				}
			}
			else {
				g.addEdge(from, to);
			}
		}
		degs[from]--;
		degs[to]--;
		totalDeg -= 2;

		// erase bigger index first
		if (degs[to] == 0)
			nodes.erase(nodes.begin() + toi);
		if (from != to && degs[from] == 0)
			nodes.erase(nodes.begin() + fromi);

	}
	return g;
}